/**
 * Copyright (C) @2014 Webank Group Holding Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.webank.framework.data.jedis.pool;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import cn.webank.framework.data.jedis.strategy.DefaultIndexStrategy;
import cn.webank.framework.data.jedis.strategy.IndexStrategy;
import cn.webank.framework.utils.WeBankEncryptUtil;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.Protocol;

/**
 * 一组JedisPool的cluster
 * 
 * @author jonyang
 *
 */
public class MultiJedisShardedPool implements InitializingBean {

	/**
	 * 一组JedisPool，类似一个partition
	 */
	private List<JedisPartition> jedisPartitions;

	/**
	 * 分组策略
	 */
	private IndexStrategy modStrategy;

	/**
	 * jedis配置
	 */
	private JedisPoolConfig jedisPoolConfig;

	/**
	 * master名称，在setinels模式用到
	 */
	private String masterName;

	/**
	 * 哨兵串,格式: ip1:port1,ip2:port2;ip3:port3,ip4:port4,每组用;分隔，同一组内不同实例用,分隔
	 */
	private String sentinels;

	/**
	 * redis串,格式:ip1:port1,ip2:port2;ip3:port3,ip4:port4,每组用;分隔，同一组内不同实例用,分隔
	 */
	private String jedisShardInfos;
	
	/**
	 * 认证口令
	 */
	private String auth;
	
	/**
	 * 发布平台的公钥证书文件
	 */
	private String sysPublicKeyFilePath;

	/**
	 * 应用私钥证书文件
	 */
	private String appPrivateKeyFilePath;

	/**
	 * 自定义的customJedisSentinelPool类名，用于监听系统事件
	 */
	private String customJedisSentinelPool;// "cn.webank.framework.data.jedis.pool.WeBankJedisSentinelPool";

	private static final String PARTITION_SEP = ";";
	private static final String INSTANCE_SEP = ",";
	private static final String IP_PORT_SEP = ":";

	public MultiJedisShardedPool() {
		this.jedisPartitions = new ArrayList<JedisPartition>();
	}

	/**
	 * @return the jedisPoolConfig
	 */
	public JedisPoolConfig getJedisPoolConfig() {
		return jedisPoolConfig;
	}

	/**
	 * @param jedisPoolConfig
	 *            the jedisPoolConfig to set
	 */
	public void setJedisPoolConfig(JedisPoolConfig jedisPoolConfig) {
		this.jedisPoolConfig = jedisPoolConfig;
	}

	/**
	 * @return the masterName
	 */
	public String getMasterName() {
		return masterName;
	}

	/**
	 * @param masterName
	 *            the masterName to set
	 */
	public void setMasterName(String masterName) {
		this.masterName = masterName;
	}

	/**
	 * @return the sentinels
	 */
	public String getSentinels() {
		return sentinels;
	}

	/**
	 * @param sentinels
	 *            the sentinels to set
	 */
	public void setSentinels(String sentinels) {
		Assert.notNull(sentinels, "sentinels config must not be null");
		this.sentinels = sentinels;
	}

	/**
	 * @return the jedisShardInfos
	 */
	public String getJedisShardInfos() {
		return jedisShardInfos;
	}

	/**
	 * @param jedisShardInfos
	 *            the jedisShardInfos to set
	 */
	public void setJedisShardInfos(String jedisShardInfos) {
		Assert.notNull(jedisShardInfos,
				"jedisShardInfos config must not be null");
		this.jedisShardInfos = jedisShardInfos;
	}

	/**
	 * @return the modStrategy
	 */
	public IndexStrategy getModStrategy() {
		return modStrategy;
	}

	/**
	 * @param modStrategy
	 *            the modStrategy to set
	 */
	public void setModStrategy(IndexStrategy modStrategy) {
		this.modStrategy = modStrategy;
	}

	/**
	 * @return the jedisShardedPools
	 */
	public List<JedisPartition> getJedisPartitions() {
		return jedisPartitions;
	}

	/**
	 * @param jedisPartitions
	 *            the jedisPartitions to set
	 */
	public void setJedisPartitions(List<JedisPartition> jedisPartitions) {
		Assert.isTrue(jedisPartitions != null && jedisPartitions.size() > 0,
				"JedisShardedPool is null");
		this.jedisPartitions = jedisPartitions;
	}

	/**
	 * 根据指定key，hash之后获取其中一个JedisPartition
	 * 
	 * @param key
	 *            需要hash的关键字
	 * @return JedisShardedPool Jedis共享池
	 */
	public JedisPartition getJedisPartition(String key) {
		int mapKey = modStrategy.getIndex(key);

		JedisPartition pool = jedisPartitions.get(mapKey);
		return pool;
	}

	/**
	 * @return the customJedisSentinelPool
	 */
	public String getCustomJedisSentinelPool() {
		return customJedisSentinelPool;
	}

	/**
	 * @param customJedisSentinelPool
	 *            the customJedisSentinelPool to set
	 */
	public void setCustomJedisSentinelPool(String customJedisSentinelPool) {
		this.customJedisSentinelPool = customJedisSentinelPool;
	}

	public void destroy() {
		for (JedisPartition js : jedisPartitions) {
			js.destroy();
		}
	}

	public void refresh() throws Exception {
		afterPropertiesSet();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		// clear
		this.jedisPartitions.clear();

		// init
		String[] jedisPartitions = this.jedisShardInfos.split(PARTITION_SEP);
		String[] sentinelPartitions = this.sentinels.split(PARTITION_SEP);

		Assert.isTrue((jedisPartitions != null) && (sentinelPartitions != null)
				&& (sentinelPartitions.length == jedisPartitions.length)
				&& (sentinelPartitions.length > 0),
				"jedisShardInfos size is not equals to sentinels size");

		String decryptedAuth =
				StringUtils.hasLength(auth) ? WeBankEncryptUtil.rsaDecrypt(sysPublicKeyFilePath,
						appPrivateKeyFilePath, auth) : auth;
		
		for (int i = 0; i < jedisPartitions.length; i++) {
			JedisPartition jedisPartition = new JedisPartition();

			String jp = jedisPartitions[i];
			String[] jedisInstances = jp.split(INSTANCE_SEP);

			List<JedisShardInfo> jedises = new ArrayList<JedisShardInfo>();

			for (int j = 0; j < jedisInstances.length; j++) {
				String[] host = jedisInstances[j].split(IP_PORT_SEP);
				Assert.isTrue(host != null && host.length == 2,
						"jedisShardInfos config is error");
				String ip = host[0];
				String port = host[1];

				JedisShardInfo jedis = new JedisShardInfo(ip,
						Integer.parseInt(port), this.masterName);
				if (StringUtils.hasLength(decryptedAuth))
					jedis.setPassword(decryptedAuth);
				jedises.add(jedis);

			}

			String sp = sentinelPartitions[i];
			String[] sentinelInstances = sp.split(INSTANCE_SEP);
			Set<String> sentinels = new HashSet<String>();

			for (int j = 0; j < sentinelInstances.length; j++) {
				sentinels.add(sentinelInstances[j]);
			}

			WeBankShardedJedisPool webankShardedJedisPool = new WeBankShardedJedisPool(
					this.jedisPoolConfig, jedises);
			jedisPartition.setWeBankShardedJedisPool(webankShardedJedisPool);
			JedisSentinelPool jedisSentinelPool = null;

			if (StringUtils.hasText(this.customJedisSentinelPool)) {
				Class<?> claz = Class.forName(this.customJedisSentinelPool);
				if (StringUtils.hasLength(decryptedAuth)) {
					Class<?>[] classArray = new Class<?>[] { String.class,
							Set.class, GenericObjectPoolConfig.class, int.class, String.class };
					Constructor<?> declaredConstructor = claz
						.getDeclaredConstructor(classArray);
					jedisSentinelPool = (JedisSentinelPool) declaredConstructor
							.newInstance(this.masterName, sentinels,
									this.jedisPoolConfig, Protocol.DEFAULT_TIMEOUT, decryptedAuth);
				} else {
					Class<?>[] classArray = new Class<?>[] { String.class,
							Set.class, GenericObjectPoolConfig.class, int.class };
					Constructor<?> declaredConstructor = claz
						.getDeclaredConstructor(classArray);
					jedisSentinelPool = (JedisSentinelPool) declaredConstructor
							.newInstance(this.masterName, sentinels,
									this.jedisPoolConfig, Protocol.DEFAULT_TIMEOUT);
				}
				
				jedisPartition.setJedisSentinelsPool(jedisSentinelPool);
			} else {
				if (StringUtils.hasLength(decryptedAuth)) {
					jedisSentinelPool = new JedisSentinelPool(this.masterName,
							sentinels, this.jedisPoolConfig,
							Protocol.DEFAULT_TIMEOUT, decryptedAuth);
				} else {
					jedisSentinelPool = new JedisSentinelPool(this.masterName,
							sentinels, this.jedisPoolConfig,
							Protocol.DEFAULT_TIMEOUT);
				}
				jedisPartition.setJedisSentinelsPool(jedisSentinelPool);
			}

			this.jedisPartitions.add(jedisPartition);

		}

		modStrategy = new DefaultIndexStrategy(this.jedisPartitions.size());

	}

	public String getAuth() {
		return auth;
	}

	public void setAuth(String auth) {
		this.auth = auth;
	}

	public String getSysPublicKeyFilePath() {
		return sysPublicKeyFilePath;
	}

	public void setSysPublicKeyFilePath(String sysPublicKeyFilePath) {
		this.sysPublicKeyFilePath = sysPublicKeyFilePath;
	}

	public String getAppPrivateKeyFilePath() {
		return appPrivateKeyFilePath;
	}

	public void setAppPrivateKeyFilePath(String appPrivateKeyFilePath) {
		this.appPrivateKeyFilePath = appPrivateKeyFilePath;
	}

}
